<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>AI 聊天</title>
    <!-- 引入代码高亮库 -->
    <!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css"> -->
    <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script> -->
    <!-- <script src="http://pfo.triplint.com/js/vue.js"></script> -->
    <link rel="stylesheet" href="default.min.css">
    <script src="highlight.min.js"></script>
    <script src="vue.js"></script>
    <script src="underscore.js"></script>

    <style>
        /* 基础样式 */
        h1,h2,h3,h4,h5,h6,h7,h8,ul,ol {
            margin: 8px 0
        }
        body {
            margin: 0;
            padding: 20px;
            background-color: #e4e4e4;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            color: #2f2f2f;
        }

        .divider::before {
            content: "";
            display: block;
            width: 80%;
            margin: 16px 0;
            border-bottom: 0.5px solid #3c3c3c
        }

        .markup {
            white-space: normal;
        }

        div.temp {
            opacity: 0.7;
            font-style: italic;
            font-size: 11px;
            white-space: pre-wrap;
            margin-bottom: 12px;
        }

        think, div.thinking {
            opacity: 0.7;
            font-style: italic;
            font-size: 12px;
            white-space: pre-wrap;
        }

        /* 聊天容器 */
        .chat-container {
            max-width: 800px;
            margin: 0 auto;
            background: #ffffff;
            color: #2f2f2f;
            border-radius: 12px;
            box-shadow: 0 2px 12px rgba(0,0,0,0.1);
            height: 95vh;
            display: flex;
            flex-direction: column;
        }

        /* 消息展示区域 */
        .chat-messages {
            flex: 1;
            padding: 20px;
            overflow-y: auto;
            display: flex;
            flex-direction: column;
            gap: 12px;
        }

        /* 消息气泡 */
        .message {
            max-width: 85%;
            padding: 12px 16px;
            border-radius: 8px;
            animation: fadeIn 0.3s ease-in; 
            font-size: small;
            box-shadow: 0 2px 12px rgba(0,0,0,0.5);
            position: relative;
        }

        .message-btn {
            position: absolute;
            bottom: 0;
            right: 0;
            padding: 4px;
            background: #0E4472;
            border-bottom-right-radius: 8px;
            color: #eee;
            border-top-left-radius: 8px;
            font-size: small;
        }

        .message-raw-btn {
            position: absolute;
            top: 0;
            right: 0;
            padding: 4px;
            background: #0E4472;
            border-top-right-radius: 8px;
            color: #eee;
            border-bottom-left-radius: 8px;
            font-size: small;
        }

        .user-message {
            background: #007AFF;
            color: white;
            align-self: flex-end;
            white-space: pre-wrap;
        }

        .bot-message {
            background: #F2F2F7;
            color: #1C1C1E;
            align-self: flex-start;
            padding-bottom: 24px;
        }

        span.quote {
            color: #800;
            background-color: #f3f3f3;
            font-style: italic;
            padding: 0 4px;
            border-radius: 4px;
            /* border: solid 1px; */
            margin: 1.2px;
        }

        /* 输入区域 */
        .input-container {
            padding: 8px;
            border-top: 1px solid #E5E5EA;
            display: flex;
            flex-direction: column;
            gap: 4px;
            background-color: #3d3d3d;
            border-bottom-left-radius: 12px;
            border-bottom-right-radius: 12px;
        }

        div.models { 
            display: flex;
            flex-direction: row;
            gap: 4px;
            padding: 2px 8px;
            overflow-x: auto;
        }

        span.badge {
            padding: 8px 16px;
            background-color: #4C6B89;
            color: white;
            border-radius: 8px;
            opacity: 0.5;
            font-size: small;
        }

        span.active {
            /* font-weight: bold; */
            background-color: #4C6B89;
            color: #DDDBCE;
            opacity: 1;
        }

        #message-input {
            flex: 1;
            padding: 16px;
            border: 1px solid #E4E4E4;
            border-radius: 12px;
            font-size: small;
            outline: none;
            background-color: white;
            color: #3d3d3d;
        }

        #send-button {
            padding: 12px 24px;
            background: #4C6B89;
            color: #DDDBCE;
            border: none;
            border-radius: 12px;
            cursor: pointer;
            transition: background 0.2s;
        }

        #send-button:hover {
            background: #0063CC;
        }

        /* 加载动画 */
        .loading {
            display: inline-block;
            padding: 12px 16px;
            background: #F2F2F7;
            border-radius: 18px;
            color: #8E8E93;
        }

        .dot-flashing {
            position: relative;
            width: 6px;
            height: 6px;
            border-radius: 50%;
            background-color: #8E8E93;
            animation: dotFlashing 1s infinite linear;
        }

        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(10px); }
            to { opacity: 1; transform: translateY(0); }
        }

        @keyframes dotFlashing {
            0% { background-color: #8E8E93; }
            50%, 100% { background-color: rgba(142,142,147,0.2); }
        }

        /* 保留原有样式基础上新增以下内容 */
        .message pre {
            background: #f8f8f8;
            padding: 12px;
            border-radius: 6px;
            overflow-x: auto;
            box-shadow: 0 2px 12px rgba(0,0,0,0.2);
        }

        .message code {
            font-family: 'Menlo', monospace;
            font-size: 14px;
        }

        .message table {
            border-collapse: collapse;
            margin: 12px 0;
        }

        .message td, .message th {
            border: 1px solid #ddd;
            padding: 8px;
        }

        ul.decimal {
            list-style-type: decimal;
        }

        li.i1 {
            margin-left: 8px;
        }
        li.i2 {
            margin-left: 16px;
        }
        li.i1 {
            margin-left: 24px;
        }

        
    </style>
</head>
<body>
    <div id="main-div" class="chat-container">
        <div class="chat-messages" id="chat-messages">
            <template v-for="msg in messages">
                <div :class="`thinking message ${msg.isUser ? 'user-message' : 'bot-message'}`" v-if="!!msg.thinking.trim()" v-on:click="msg.tfolded=!msg.tfolded;">{{ msg.tfolded ? (msg.thinking.substr(0, 6) + "...") : msg.thinking }}</div>
                <template v-if="msg.isUser">
                    <div class="message user-message" v-html="msg.content"></div>
                </template>
                <template v-else>
                    <div class="message bot-message">
                        <div v-if="msg.rawPresent" style="white-space: pre-wrap;">{{ msg.raw.trim() }}</div>
                        <div v-else v-html="msg.content"></div>    
                        <div class="message-raw-btn" v-on:click="msg.rawPresent=!msg.rawPresent;">
                            <span>{{ msg.rawPresent ? "MD" : "RAW" }}</span>
                        </div>
                        <div class="message-btn" v-on:click="cp2clipboard(msg.raw);"><span>COPY</span></div>
                    </div>
                </template>
                
            </template>
            
            <div class="message loading bot-message" v-if="loading">
                <div class="temp" v-if="!!previewText">{{ previewText }}</div>
                <div class="dot-flashing"></div>
            </div>
        </div>
        
        <div class="input-container">
            <div class="models">
                <template v-for="m in models">
                    <span class="badge" v-on:click="selectModel(m)" :class="model==m ? 'active' : ''">
                        {{ m }}
                    </span>
                </template>
                
            </div>
            <div style="display:flex;flex-direction:row;gap: 4px;width: 100%">
            <textarea v-model="sendText"  type="text" 
                   id="message-input" rows=1
                   placeholder="输入消息..."
                   autocomplete="off"
                   autofocus></textarea>
            <button id="send-button">发送</button>
            </div>
        </div>
    </div>

    <script>
        let models = ['deepseek-r1:8b', 'deepseek-coder-v2', 'llama3.2']
        let chatHistory = []; // 存储完整的对话历史 {role, content}
        const chatMessages = document.getElementById('chat-messages');
        const messageInput = document.getElementById('message-input');
        const sendButton = document.getElementById('send-button');
    

        // ================== 新增 Wiki 转换函数 ==================
        function wikiToHtml(text) {

            // 转换代码块
            let codes = []
            let quotes = []
            text = text.trim()
            text = text.replace(/``` *(\w+)? *\n([\s\S]*?)```/g, (match, lang, code) => {
                let idx = codes.length;
                codes.push({ 
                    "lang": lang || '', 
                    "code": code.trim().replace(/</g, '&lt;').replace(/>/g, '&gt;')
                })
                return `<coding>${idx}</coding>`
            });

            text = text.replace(/`([^`]*?)`/g, (match, quote) => {
                let idx = quotes.length;
                quotes.push(quote.trim().replace(/</g, '&lt;').replace(/>/g, '&gt;'))
                return `<quote>${idx}</quote>`
            });

            // 转换标题
            text = text.replace(/^===(.+?)===/gm, '<h3>$1</h3>');
            text = text.replace(/^==(.+?)==/gm, '<h4>$1</h4>');
            text = text.replace(/^=(.+?)=/gm, '<h5>$1</h5>');
            text = text.replace(/^##### +([\s\S]*?)$/gm, '<h7>$1</h7>');
            text = text.replace(/^#### +([\s\S]*?)$/gm, '<h6>$1</h6>');
            text = text.replace(/^### +([\s\S]*?)$/gm, '<h5>$1</h5>');
            text = text.replace(/^## +([\s\S]*?)$/gm, '<h4>$1</h4>');
            text = text.replace(/^# +([\s\S]*?)$/gm, '<h3>$1</h3>');
            // 分割线
            text = text.replace(/^-{3,} *$/gm, '<div class="divider"></div>');
            text = text.replace(/\n+<div class="divider"><\/div>\n+/g, "\n<div class=\"divider\"></div>\n")

            // 转换列表
            text = text.replace(/\n( *[\*-] ([^\n]+)\n){1,}/gs, (match, link, text) => {
                match = match.replace(/^[\*-] (.+)/gm, '<li class="i0">$1</li>');
                match = match.replace(/^ [\*-] (.+)/gm, '<li class="i1">$1</li>');
                match = match.replace(/^  [\*-] (.+)/gm, '<li class="i2">$1</li>');
                match = match.replace(/^   [\*-] (.+)/gm, '<li class="i3">$1</li>');
                return `\n<ul>${match}</ul>`;
            });

            text = text.replace(/\n([0-9]{1,}\. ([^\n]+)\n){2,}/gs, (match, link, text) => {
                
                match = match.replace(/^[0-9]{1,}\. (.+)/gm, '<li class="i0">$1</li>');
                console.log(match)
                return `\n<ol>${match}</ol>`;
            });
            
            // text = text.replace(/^[\*-] (.+)/gm, '<li class="i0">$1</li>');
            // text = text.replace(/^ [\*-] (.+)/gm, '<li class="i1">$1</li>');
            // text = text.replace(/^  [\*-] (.+)/gm, '<li class="i2">$1</li>');
            // text = text.replace(/^   [\*-] (.+)/gm, '<li class="i3">$1</li>');
            // text = text.replace(/(<li class="i[0-9]">.*<\/li>)/gs, '<ul>$1</ul>');
            
            // text = text.replace(/^[0-9]+\. (.+)/gm, '<li>$1</li>').replace(/(<li>.*<\/li>)/gs, '<ol>$1</ol>');

            // 转换表格
            text = text.replace(/{\|([\s\S]*?)\|}/g, (match, content) => {
                return `<table>${content.replace(/^\|-(.+)/gm, '<tr><td>$1</td></tr>')}</table>`;
            });

            text = text.replace(/\n((\|[^\|]*\|){1,}\n){1,}/g, (match) => {
                console.log(match)
                return match;
            });

            // 转换链接和粗体
            text = text.replace(/\[\[([^\|]+?)\|?(.+?)?\]\]/g, (match, link, text) => {
                return `<a href="${link}">${text || link}</a>`;
            });
            text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
            text = text.replace(/'''(.+?)'''/g, '<strong>$1</strong>');
            text = text.replace(/\n{2,}/g, "\n</br>")

            text = text.replace(/`(.+?)`/g, '<span class="quote">$1</span>');

            for(let i = quotes.length; i--;){
                text = text.replace(new RegExp('<quote>' + i + '<\\/quote>', 'g'), `<span class="quote">${quotes[i]}</span>`)
            }

            for(let i = codes.length; i--;){
                const coding = codes[i]
                text = text.replace(new RegExp('<coding>' + i + '<\\/coding>', 'g'), `<pre ref="codeBlock" class="code ${coding['lang']}">${coding['code']}</pre>`)
            }

            return `<div class="markup">${text}</div>`;
        }

        function addMessage(content, isUser = false) {
            const messageDiv = document.createElement('div');
            messageDiv.className = `message ${isUser ? 'user-message' : 'bot-message'}`;
            
            // 仅对机器人消息转换 Wiki 格式
            if (!isUser) {
                messageDiv.innerHTML = wikiToHtml(content);
                // 高亮代码块
                messageDiv.querySelectorAll('pre.code').forEach(block => {
                    hljs.highlightElement(block);
                });
            } else {
                messageDiv.textContent = content;
            }
            
            chatMessages.appendChild(messageDiv);
            chatMessages.scrollTop = chatMessages.scrollHeight;
        }

        // 显示加载状态
        function showLoading() {
            const loadingDiv = document.createElement('div');
            loadingDiv.className = 'message loading bot-message';
            // if(text) {
            //     text = wikiToHtml(text)
            //     loadingDiv.innerHTML = `<div>${text}</div><div class="dot-flashing"></div>`;
            //     loadingDiv.querySelectorAll('pre.code').forEach(block => {
            //         hljs.highlightElement(block);
            //     });
            // }else{
            //     loadingDiv.innerHTML = `<div class="dot-flashing"></div>`;
            // }

            loadingDiv.innerHTML = `<div class="temp"></div><div class="dot-flashing"></div>`;
            
            chatMessages.appendChild(loadingDiv);
            chatMessages.scrollTop = chatMessages.scrollHeight;
            return loadingDiv;
        }

        function trimHistory(history, maxRounds = 5) {
            if (history.length > maxRounds * 2) {
                return history.slice(-maxRounds * 2);
            }
            return history;
        }

        function extractFirstAndRemoveFirstThinkTag(str) {
            // 匹配并替换第一个<think>标签
            str = str || ""
            let extracted = [];
            const cleanedStr = str.replace(/<think[^<>]*>([\s\S]*?)<\/think>/, (match, p1) => {
                extracted.push(p1);
                return ''; // 替换第一个匹配项为空
            });
            return { extracted, cleanedStr };
        }

        async function sendToOllama() {
            const loading = showLoading();

            try {
                chatHistory = trimHistory(chatHistory, 32);
                const response = await fetch('http://localhost:11434/api/chat', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        // model: 'deepseek-coder-v2', 
                        model: 'deepseek-r1:8b',
                        messages: chatHistory, 
                        stream: true
                    })
                });

                const reader = response.body.getReader();
                let partial = '';
                
                while (true) {
                    const { done, value } = await reader.read();
                    if (done) break;
                    // 
                    let data = JSON.parse(new TextDecoder().decode(value))
                    
                    partial += data.message.content.trim();
                    if(partial.startsWith('<think')) {
                        // let r = extractFirstAndRemoveFirstThinkTag(partial)
                        // if(r.extracted.length > 0 ){
                        //     loading.querySelectorAll('div.temp').forEach(block => {
                        //         let html = `<think>${r.extracted[0]}</think>` + wikiToHtml(r.cleanedStr) 
                        //         block.innerHTML = html
                        //     });
                        //     loading.querySelectorAll('pre.code').forEach(block => {
                        //         hljs.highlightElement(block);
                        //     });
                        // }else{
                        //     loading.querySelectorAll('div.temp').forEach(block => {
                        //         block.innerHTML = partial
                        //     });
                        // }
                        loading.querySelectorAll('div.temp').forEach(block => {
                            block.innerHTML = partial.replace(/<\/think>/, '</think></br>')
                        });
                    }else{
                        // 解析分块数据并更新消息
                        loading.querySelectorAll('div.temp').forEach(block => {
                            block.innerHTML = wikiToHtml(partial)
                        });
                        loading.querySelectorAll('pre.code').forEach(block => {
                            hljs.highlightElement(block);
                        });
                    }
                    
                }

                // if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
                // const data = await response.json();

                chatMessages.removeChild(loading);
                
                // chatHistory.push({ role: 'assistant', content: data.message.content });
                chatHistory.push({ role: 'assistant', content: partial.replace(/<think[^<>]*>.*<\/think>/g, '') });
                addMessage(partial);
            } catch (error) {
                addMessage(`错误: ${error.message}`, false);
                chatMessages.removeChild(loading);
            }
        }

        
        let vm = new Vue({
            el: "#main-div",
            data: {
                models: ['deepseek-r1:8b', 'deepseek-coder-v2', 'llama3.2', 'deepseek-coder'],
                model: "deepseek-r1:8b",
                chatHistory: [],
                messages: [],
                loading: false,
                sendText: "",
                partial: ""
            },
            mounted: function(){
                const self = this;
                const sendButton = document.getElementById('send-button');
                const messageInput = document.getElementById('message-input');
                sendButton.addEventListener('click', async () => {
                    if (self.sendText) {
                        self.chatHistory.push({ role: 'user', content: self.sendText });
                        self.addMessage(self.sendText, true);
                        self.sendText = ""
                        self.sendToOllama();
                    }else if(self.chatHistory.length > 1) {
                        let last = self.chatHistory.pop()
                        while(last.role !== 'user')
                        {
                            last = self.chatHistory.pop()
                        }

                        self.chatHistory.push(last)
                        self.addMessage(last.content, true);
                        self.sendToOllama();
                        
                    }
                    
                });

                messageInput.addEventListener('keypress', (e) => {
                    if (e.key === 'Enter' && !e.shiftKey) {
                        e.preventDefault();
                        sendButton.click();
                    }
                });
            },
            computed: {
                previewText() {
                    return this.partial.trim().replace(/^<think>\s*<\/think>/g, "").trim()
                }
            },
            updated() {
                this.$nextTick(() => {
                    document.getElementById('chat-messages').querySelectorAll('pre.code').forEach(block => {
                        hljs.highlightElement(block);
                        block.className=""
                    });
                    document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight;
                });
            },
            methods: {
                cp2clipboard(content) {
                    // 检查权限
                    if (navigator.clipboard) {
                        navigator.clipboard.writeText(this.md2wiki(content.trim())).then(() => {
                            console.log("内容已成功复制到剪切板");
                        }).catch(err => {
                            console.error("复制失败:", err);
                        });
                    } else {
                        alert("您的浏览器不支持该操作，请使用现代浏览器如Chrome、Firefox等。");
                    }
                },
                md2wiki(text) { 
                    let codes = []
                    let quotes = []
                    text = text.trim()
                    text = text.replace(/``` *(\w+)? *\n([\s\S]*?)```/g, (match, lang, code) => {
                        let idx = codes.length;
                        codes.push({ 
                            "lang": lang || '', 
                            "code": code.trim()
                        })
                        return lang ? `{{{#!${lang}\n${code.trim()}\n}}}` : `{{{\n${code.trim()}\n}}}`
                    });

                    // 转换标题
                    text = text.replace(/^====(.+?)====/gm, '==== $1');
                    text = text.replace(/^===(.+?)===/gm, '=== $1');
                    text = text.replace(/^==(.+?)==/gm, '== $1');
                    text = text.replace(/^=(.+?)=/gm, '= $1');
                    text = text.replace(/^##### +([\s\S]*?)$/gm, '===== $1');
                    text = text.replace(/^#### +([\s\S]*?)$/gm, '==== $1');
                    text = text.replace(/^### +([\s\S]*?)$/gm, '=== $1');
                    text = text.replace(/^## +([\s\S]*?)$/gm, '== $1');
                    text = text.replace(/^# +([\s\S]*?)$/gm, '= $1');
                    text = text.replace(/^-{3,} *$/gm, '------');

                    // 转换链接和粗体
                    text = text.replace(/\[\[([^\|]+?)\|?(.+?)?\]\]/g, (match, link, text) => {
                        return `[${link} ${text} ${link}]`
                    });
                    // text = text.replace(/'''(.+?)'''/g, '<strong>$1</strong>');
                    text = text.replace(/\*\*(.+?)\*\*/g, "'''$1'''");

                    return text;
                },
                selectModel(model) {
                    this.model = model
                    this.chatHistory = []
                    this.messages = []
                },
                addMessage(msg, isUser){
                    // console.log(msg)
                    for(let i = this.messages.length; i--;){
                        this.messages[i]['tfolded'] = true
                    }
                    if(isUser){
                        this.messages.push({
                            raw: msg,
                            content: msg,
                            thinking: "",
                            isUser: !!isUser,
                            tfolded: false, 
                            cfolded: false,
                            rawPresent: false
                        })
                    }else{
                        let r = extractFirstAndRemoveFirstThinkTag(msg)

                        this.messages.push({ 
                            raw: r.cleanedStr,
                            content: wikiToHtml(r.cleanedStr).trim(),
                            thinking: r.extracted.length > 0 ? r.extracted[0].trim() : "",
                            isUser: !!isUser,
                            tfolded: false, 
                            cfolded: false,
                            rawPresent: false
                        })
                    }
                    
                },
                sendToOllama(next) {
                    let self = this;
                    self.chatHistory = trimHistory(self.chatHistory, 8);
                    self.loading = true
                    next = next || function(){}
                    fetch('http://localhost:11434/api/chat', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({
                            model: self.model,
                            messages: self.chatHistory, 
                            stream: true
                        })
                    })
                    .then(response => {
                        const reader = response.body.getReader();
                        let partial = ""
                        // 递归读取流数据
                        function readChunk() {
                            reader.read().then(({ done, value }) => {
                                if (done) {
                                    self.loading = false
                                    self.chatHistory.push({ role: 'assistant', content: self.partial.replace(/<think[^<>]*>[\s\S]*?<\/think>/g, '') });
                                    self.addMessage(self.partial, false)
                                    self.partial = ""
                                    next()
                                }else{
                                    const data = JSON.parse(new TextDecoder().decode(value));
                                    if(data.message){
                                        self.partial += data.message.content;
                                    }else{
                                        console.log(JSON.stringify(data))
                                    }
                                    // console.log(self.partial)
                                    readChunk();
                                }
                                // const data = JSON.parse(new TextDecoder().decode(value));
                                // partial += data.message.content.trim();

                                // if (partial.startsWith('<think')) {
                                //     loading.querySelectorAll('div.temp').forEach(block => {
                                //         block.innerHTML = partial.replace(/<\/think>/, '</think></br>');
                                //     });
                                // } else {
                                //     loading.querySelectorAll('div.temp').forEach(block => {
                                //         block.innerHTML = wikiToHtml(partial);
                                //     });
                                //     loading.querySelectorAll('pre.code').forEach(block => {
                                //         hljs.highlightElement(block);
                                //     });
                                // }

                                
                            })
                            .catch(error => {
                                self.loading = false
                                // console.log(new TextDecoder().decode(value))
                                self.addMessage(error.message, false)
                                next()
                            });
                        }

                        readChunk()
                    })
                    .catch(error => {
                        self.loading = false
                        self.addMessage(error.message, false)
                        next()
                    })
                }
            }
        })
    </script>

</body>
</html>
